package hit.core;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import hit.reports.Visitor;
import java.util.Comparator;
import java.util.PriorityQueue;

/**
 * Stores all {@link Item}s from the HIT system. The removed Items index is utilized heavily by
 * the reporting system.
 * 
 */
@SuppressWarnings("serial")
public class ItemIndex extends HITBase {
    private Map<Barcode, Item> items;
    private Map<ProductContainer, Set<Item>> itemsByContainer;
    private List<Item> removedItems;
    
    /**
     * Default Constructor initializes ItemIndex to empty
     * */
    public ItemIndex() {
    	items = new HashMap<Barcode, Item>();
    	itemsByContainer = new HashMap<ProductContainer, Set<Item>>();
    	
    	removedItems = new ArrayList<Item>();
    }
	
	/**
	 * Retrieves the Item that matches this Barcode
	 * 
	 * @param barcode A valid {@link Item} {@link Barcode}
	 * @return the {@link Item} that matches the Item {@link Barcode}
	 */
	public Item GetItemByBarcode(Barcode barcode){
		return items.get(barcode);
	}
	
	/**
	 * Get a Set of Items contained in a ProductContainer
	 * @param productContainer the ProductContainer to search
	 * @return the Set of Items in the ProductContainer
	 * */
	public Set<Item> GetItemsByProductContainer(ProductContainer productContainer) {
		return itemsByContainer.get(productContainer);
	}
	
	/**
	 * Add an Item to the ItemIndex
	 * @param item the Item to add
	 * @throws IllegalArgumentException thrown if !canAddItem
	 * */
	public void AddItem(Item item) throws IllegalArgumentException {
		if (!CanAddItem(item))
			throw new IllegalArgumentException("Invalid Items cannot be added to ItemIndex");
		items.put(item.GetBarcode(), item);
		AddItemToContainerIndex(item);
		
		//add the item to the product container and the product index
		ProductIndex productIndex = HomeInventoryTracker.Instance().GetProductIndex(); 
		
		if (productIndex.CanAddProductToContainer(item.GetProduct(), item.GetContainer())) {
			productIndex.AddProductToContainer(item.GetProduct(), item.GetContainer());
		}
	}
	
	/**
	 * Undo removing an Item from the ItemIndex
	 * @param item the Item to add
	 * @throws IllegalArgumentException thrown if !canAddItem
	 * */
	public void UndoRemoveItem(Item item) throws IllegalArgumentException {
		if (!CanAddItem(item))
			throw new IllegalArgumentException("Invalid Items cannot be added to ItemIndex");
		items.put(item.GetBarcode(), item);
		AddItemToContainerIndex(item);
		
		//add the item to the product container and the product index
		ProductIndex productIndex = HomeInventoryTracker.Instance().GetProductIndex(); 
		
		if (productIndex.CanAddProductToContainer(item.GetProduct(), item.GetContainer())) {
			productIndex.AddProductToContainer(item.GetProduct(), item.GetContainer());
		}
		
		//remove the undone item from the removed items index
		removedItems.remove(item);
		item.SetExit(null);
	}
	
	/**
	 * Returns if the Item can be added to the ItemIndex
	 * @param item the Item to check
	 * @return whether the Item can be added. An Item can be added if the Barcode is not
	 * already contained in the Index and it is valid.
	 */
	public boolean CanAddItem(Item item) {
		if (!Item.IsValid(item) || items.containsKey(item.GetBarcode()))
			return false;
		return true;
	}
	
	/**
	 * Determines if the Item can be removed
	 * @param item the Item to check
	 * @return whether the Item can be removed
	 * */
	public boolean CanRemoveItem(Item item) {
		if (!Item.IsValid(item) || item.IsRemoved() || !items.containsValue(item))
			return false;
		return true;
	}
	
	public void RemoveItem(Item item)
	{
		RemoveItem(item, new Date());
	}
	
	/**
	 * Removes an Item from the ItemIndex
	 * @param item the Item to remove
	 * @throws IllegalArgumentException thrown if the Item is not valid or if Item.isRemoved()
	 * */
	public void RemoveItem(Item item, Date exitDate) {
		if (!CanRemoveItem(item)) {
			throw new IllegalArgumentException("The Item cannot be removed");
		}
		
		if (!removedItems.contains(item))
		{
			removedItems.add(item);
		}
		
		//remove from the items index
		item.Remove(exitDate);
		items.remove(item.GetBarcode());
		itemsByContainer.get(item.GetContainer()).remove(item);
	}
	
	/**
	 * Add an Item directly into the removed items index
	 * @param item the Item to remove
	 * */
	public void AddToRemovedIndex(Item item) {
		removedItems.add(item);
	}
	
	/**
	 * Undo adding an Item to the ItemIndex. This is basically like the RemoveItem method,
	 * except that it doesn't add the item to the removed Items Index
	 * @param item the Item to remove
	 * @throws IllegalArgumentException thrown if the Item is not valid or if Item.isRemoved()*/
	public void UndoAddItem(Item item) {
		if (!CanRemoveItem(item)) {
			throw new IllegalArgumentException("The Item cannot be removed");
		}
		
		//remove from the items index
		items.remove(item.GetBarcode());
		itemsByContainer.get(item.GetContainer()).remove(item);
	}
	
	/**
	 * Returns whether an {@link Item} can be updated or not
	 * @param before the Item before the Item was updated
	 * @param after the Item after the updates
	 * @return whether the Item can be updated
	 *  */
	public boolean CanUpdateItem(Item before, Item after) {
		if (!items.containsValue(before) || !Item.IsValid(after))
			return false;
		return true;
	}
	
	/**
	 * Update an Item in the ItemIndex
	 * @param before the Item before the Item was updated
	 * @param after the Item after the updates
	 * @throws IllegalArgumentException thrown if the before is not in the ItemIndex or
	 * if after is not valid
	 * */
	public void UpdateItem(Item before, Item after) throws IllegalArgumentException {
		if (!CanUpdateItem(before, after))
			throw new IllegalArgumentException("Unable to update Item");
		
			//change the barcode index
			items.remove(before.GetBarcode());
			items.put(after.GetBarcode(), after);
		
			//change the product container index
			itemsByContainer.get(before.GetContainer()).remove(before);
			AddItemToContainerIndex(after);
	}
	
	/**
	 * Returns an Iterator to iterate through the Items in the ItemIndex
	 * @return the Iterator for the ItemIndex
	 * */
	public Iterator<Item> GetItemIterator() {
		return new Iterator<Item>() {
			Iterator<Barcode> keys = items.keySet().iterator();
			
			@Override
			public boolean hasNext() {
				return keys.hasNext();
			}

			@Override
			public Item next() {
				return items.get(keys.next());
			}

			@Override
			public void remove() {
				throw new UnsupportedOperationException("Remove is not supported");
			}
			
		};
	}
	
	/**
	 * Returns an Iterator to iterate through the RemovedItems in the ItemIndex
	 * @return the Iterator for the RemovedItemIndex
	 * */
	public Iterator<Item> GetRemovedItemIterator() {
		
		Comparator<Item> itemCompare  = new Comparator<Item>() {
	        @Override
	        public int compare(Item n1, Item n2){
				return n1.GetExit().compareTo(n2.GetExit());
	        }
	    };
		
	    Collections.sort(removedItems, itemCompare);
		return removedItems.iterator();
	}
	
	/**
	 * Determines if an Item can be transferred from one ProductContainer to another
	 * @param item the Item to transfer
	 * @param from the original ProductContainer for the Item
	 * @param to the ProductContainer to move to
	 * @return whether the Item can be transferred
	 */
	public boolean CanTransferItem(Item item, ProductContainer from, ProductContainer to) {
		if (!Item.IsValid(item) || !itemsByContainer.get(from).contains(item))
			return false;
		
		//if the from container only contains 1 of the item, you should be able to 
		//transfer within the storage unit
		Iterator<Item> items = itemsByContainer.get(from).iterator();
		int count = 0;
		while (items.hasNext()) {
			if (items.next().GetProduct().equals(item.GetProduct()))
				count++;
		}
				
		//find if the item is contained in any of the "to" ProductContainer's parents
		ProductContainer parent = to;
		while (!(parent instanceof StorageUnit)) {
			parent = parent.GetParentContainer();
		}
		ProductContainer containing = HomeInventoryTracker.Instance().GetProductIndex().
				FindProductContainer(parent, item.GetProduct());
		if (from.equals(containing) && count == 1)
			return true;
		if (containing != null && !containing.equals(to)) {
			return false;
		}
		
		return true;
	}
	
	/*
	 * if (!CanTransferItem(item, from, to))
			throw new IllegalArgumentException("Cannot transfer Item");
	 */
	
	/**
	 * Transfers an {@link Item} to another {@link StorageUnit} tree.
	 * @param item the Item to transfer. Must be a valid Item.
	 * @param unit the StorageUnit to transfer to. Must be a non null, valid StorageUnit
	 * */
	public void TransferItemToTree(Item item, StorageUnit unit) throws IllegalArgumentException {
		
		if (!Item.IsValid(item) || unit == null || !unit.IsValid())
			throw new IllegalArgumentException();
		
		// Find the ProductContainer
		Product product = item.GetProduct();
		Iterator<ProductContainer> iter = product.GetProductContainerIterator();
	
		boolean success = false;
		while(iter.hasNext()){
			ProductContainer container = iter.next();
			if (container.GetRootContainer().equals(unit)){
				success = true;
				TransferItemWOCheck(item, item.GetContainer(), container);
			}
		}
		if (!success){
			
			HomeInventoryTracker.Instance().GetProductIndex().AddProductToContainer(product, unit);
			TransferItemWOCheck(item, item.GetContainer(), unit);
		}
	}
	
	/**
	 * Transfers all {@link Item}s of a certain {@link Product} from one {@link ProductContainer}
	 * to another. 
	 * @param product the Product to transfer
	 * @param initial the starting ProductContainer
	 * @param destination the destination ProductContainer
	 * */
	public void TransferAllItems(Product product, ProductContainer initial, 
			ProductContainer destination) throws IllegalArgumentException {
		
		if (initial.GetRootContainer() != destination.GetRootContainer()) {
			throw new IllegalArgumentException();
		}
		
		Iterator<Item> iter = this.GetItemsByProduct(initial, product);
		while (iter.hasNext()) {
			Item temp = iter.next();
			TransferItemWOCheck(temp, initial, destination);
		}
	}
	
	/**
	 * Transfer an {@link Item} to a {@link ProductContainer}
	 * @param item the Item to transfer
	 * @param destination the destination ProductContainer
	 * */
	public List<Item> TransferItem(Item item, ProductContainer destination)
			throws IllegalArgumentException {	
		List<Item> transferredItems = new ArrayList<Item>();
		//break into 3 cases
		StorageUnit origStorage = item.GetRootStorageUnit();
		//if destination is in the tree of item.GetRootStorageContainer
		if (origStorage.equals(destination.GetRootContainer())){
			transferredItems = new ArrayList<Item>(GetItemsByProductContainer(
					item.GetContainer()));
			TransferItemsWOCheck(GetItemsByProductContainer(item.GetContainer()), 
					item.GetProduct(), item.GetContainer(), destination);
		}
		else{
			StorageUnit newStorage = destination.GetRootContainer();
			//if product is already in destination productcontainer
			if (item.GetProduct().IsContainedBy(destination)){
				TransferItemWOCheck(item, item.GetContainer(), destination);
			}
			else{
				Iterator<ProductContainer> iter = item.GetProduct().GetProductContainerIterator();
				while (iter.hasNext()){
					ProductContainer pc = iter.next();
					//if tree of destination.GetRootContainer has item.product already
					if (pc.GetRootContainer().equals(newStorage)){
						transferredItems = new ArrayList<Item>(GetItemsByProductContainer(pc));
						TransferItemsWOCheck(GetItemsByProductContainer(pc), 
								item.GetProduct(), pc, destination);
						transferredItems.add(item);
						TransferItemWOCheck(item, item.GetContainer(), destination);
						return transferredItems;
					}
				}
				//else
				ProductIndex productIndex = HomeInventoryTracker.Instance().GetProductIndex();
				productIndex.AddProductToContainer(item.GetProduct(), destination);
				TransferItemWOCheck(item, item.GetContainer(), destination);
				transferredItems.add(item);
			}
		}
			
		return transferredItems;
			
	}
	
	private void TransferItemsWOCheck(Set<Item> items, 
			Product product, ProductContainer from, ProductContainer to) {
		
		ProductIndex productIndex = HomeInventoryTracker.Instance().GetProductIndex();
		productIndex.TransferProduct(product, to);
		Set<Item> itemsCopy = new HashSet<Item>(items);
		Iterator<Item> iter = itemsCopy.iterator();
		while (iter.hasNext()) {
			Item temp;
			try{
				temp = iter.next();
			}
			catch (Exception e){
				break;
			}
			if (temp.GetProduct().equals(product)){
				TransferItemWOCheck(temp, from, to);
			}
		}
	}
	
	/**
	 * Transfer an Item from one ProductContainer to another
	 * @param item the Item to transfer
	 * @param from the original ProductContainer for the Item
	 * @param to the ProductContainer to move to
	 * @throws IllegalArgumentException thrown if item is not valid, if item does not
	 * belong to from, or if to is not a valid move
	 * */
	private void TransferItemWOCheck(Item item, ProductContainer from, ProductContainer to) {		
		itemsByContainer.get(from).remove(item);
		item.SetContainer(to);
		AddItemToContainerIndex(item);
	}
	
	/**
	 * Returns the number of removed Items in the removed item index for testing purposes
	 * @return the number of removed Items
	 * */
	protected int NumRemovedItems() {
		return removedItems.size();
	}
	
	
	/**
	 * Adds an Item to the product container index.
	 * @param item the Item to add
	 * */
	private void AddItemToContainerIndex(Item item) {
		if (itemsByContainer.containsKey(item.GetContainer())) {
			itemsByContainer.get(item.GetContainer()).add(item);
		}
		else {
			Set<Item> itemSet = new HashSet<Item>();
			itemSet.add(item);
			itemsByContainer.put(item.GetContainer(), itemSet);
		}
	}
	
	/**
	 * Returns an Iterator for all {@link Item}s of a given {@link Product}
	 * @param product the Product to search for
	 * @return an Iterator of all Items of the Product
	 * */
	public Iterator<Item> GetItemsByProduct(Product product) {
		return GetItemsByProduct(null, product);
	}
	
	/**
	 * Returns all {@link Item}s of a given {@link Product} within a specific 
	 * {@link ProductContainer}
	 * @param container the ProductContainer to search in
	 * @param product the Product to search for
	 * @return the Iterator for the Items of type Product within the ProductContainer
	 * */
	public Iterator<Item> GetItemsByProduct(ProductContainer container, Product product){

		List<Item> results = new ArrayList<Item>();
		Iterator<Item> iter;
		
		if (container == null) {
			iter = GetItemIterator();
		}
		else
		{
			Set<Item> itemsList = GetItemsByProductContainer(container);
			
			if (itemsList == null)
				return results.iterator();
			
			iter = itemsList.iterator();
		}
		
		try {
			while (iter.hasNext()) {
				
				Item temp = iter.next();
				
				if (temp.GetProduct() == product)
					results.add(temp);
			}
		}
		catch(Exception e) {
			e.printStackTrace();
			results.clear();
		}
		
		return results.iterator();
	}
	
	/**
	 * Remove all {@link Item}s of a given {@link Product} from the removed item index.
	 * @param product the Product type of the Items to remove
	 * */
	public void DeleteFromRemovedItems(Product product) throws IllegalArgumentException {
		ProductIndex productIndex = HomeInventoryTracker.Instance().GetProductIndex();
		if (!productIndex.CanRemoveProduct(product))
			throw new IllegalArgumentException();
			
		List<Item> temp = new ArrayList<Item>(removedItems);
		
		for (Item item : temp){
			if (item.GetProduct() == product){
				removedItems.remove(item);
			}
		}
	}
	
	public void Accept(Visitor visitor){
		visitor.VisitItemIndex(this);
		Set<Barcode> keyset = items.keySet();
		for (Barcode barcode : keyset){
			items.get(barcode).Accept(visitor);
		}
	}
}
